View.java
package example;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.Labeled;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Slider;
import javafx.scene.control.TextField;
import javafx.scene.control.TextInputDialog;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.function.Consumer;
import java.util.stream.IntStream;
/**
* This program creates several kinds of UI components for testing.
*
* @author Robert C. Duvall
*/
public class View {
// constants
// use Java's dot notation, like with import, for properties files
public static final String DEFAULT_RESOURCE_PACKAGE = "example.";
public static final String DEFAULT_STYLESHEET = "/"+DEFAULT_RESOURCE_PACKAGE.replace(".", "/")+"Default.css";
public static final boolean DEBUG = true;
// choices displayed in UI
private Model myModel;
private ObservableList<String> myChoices;
private ResourceBundle myResources;
/**
* Create view in given language with given model
*/
public View (String language, Model model) {
myModel = model;
myChoices = FXCollections.observableArrayList("11", "22", "33", "44");
myResources = ResourceBundle.getBundle(DEFAULT_RESOURCE_PACKAGE + "View" + language);
}
// create different kinds of components for testing
// make package friendly so test classes can call it
public Scene makeScene (int width, int height) {
GridPane root = new GridPane();
root.setOnMouseClicked(e -> toggleHighlight(root));
// for example of using nested lookup
root.getStyleClass().add("grid-pane");
// for example of using ID lookup
root.setId("Pane");
// common action for components to keep it simple
Consumer<String> updateAction = text -> myModel.setValue(text);
// make example components
Node label = makeLabel("Label");
TextField inputField = makeInputField("InputField", updateAction);
// group all buttons
Node buttons = makeActions(
makeButton("Apply", e -> getText(inputField, updateAction)),
makeButton("Input", e -> getInput("InputPrompt", updateAction)),
makeButton("Data", e -> getData("example/choices.txt", updateAction))
);
Node slider = makeSlider("Slider", 0, 50, 100, updateAction);
Node picker = makeColorPicker("Picker", Color.BLUE, updateAction);
Node combo = makeCombo("Options", myChoices, updateAction);
Node radio = makeRadioButtons("Radio", myChoices, updateAction);
Node check = makeCheckBoxes("Check", myChoices, updateAction);
Node canvas = makeCanvas("Canvas", 100, 325, 20, updateAction);
// add elements to GUI
root.add(label, 1, 0);
root.add(inputField, 1, 1);
root.add(buttons, 1, 2);
root.add(slider, 1, 3);
root.add(picker, 1, 4);
root.add(combo, 1, 5);
root.add(radio, 1, 6);
root.add(check, 1, 7);
root.add(canvas, 2, 1, 1, 7);
Scene scene = new Scene(root, width, height);
// activate CSS styling
scene.getStylesheets().add(getClass().getResource(DEFAULT_STYLESHEET).toExternalForm());
return scene;
}
// Display given message as an error in the GUI
private void showError (String message) {
Alert alert = new Alert(AlertType.ERROR);
alert.setTitle(myResources.getString("ErrorTitle"));
alert.setContentText(message);
alert.showAndWait();
}
// convenience methods to encapsulate making components
private Node makeButton (String property, EventHandler<ActionEvent> response) {
Button result = new Button();
result.setText(myResources.getString(property));
result.setOnAction(response);
return setID(property, result);
}
private Node makeLabel (String property) {
Label label = new Label(myResources.getString(property));
myModel.addSetter(newText -> updateLabel(label, newText));
return setID(property, label);
}
private TextField makeInputField (String id, Consumer<String> response) {
TextField result = new TextField();
result.setOnAction(e -> response.accept(result.getText()));
return (TextField)setID(id, result);
}
private Node makeActions (Node ... buttons) {
HBox panel = new HBox();
panel.getStyleClass().add("button-box");
panel.getChildren().addAll(buttons);
return panel;
}
private Node makeColorPicker (String id, Color defaultChoice, Consumer<String> response) {
ColorPicker picker = new ColorPicker(defaultChoice);
picker.setOnAction(e -> response.accept(picker.getValue().toString()));
return setID(id, picker);
}
private Node makeSlider (String id, int min, int start, int max, Consumer<String> response) {
Slider slider = new Slider();
slider.setMin(min);
slider.setMax(max);
slider.setValue(start);
slider.valueProperty().addListener((o, oldValue, newValue) -> response.accept(newValue.toString()));
return setID(id, slider);
}
// make combination of choices
private Node makeCombo (String id, ObservableList<String> choices, Consumer<String> response) {
// these two components seem to work the same way --- uncomment to try different flavors
ComboBox<String> options = new ComboBox<>();
//ChoiceBox<String> options = new ChoiceBox<>();
options.valueProperty().addListener((o, oldValue, newValue) -> response.accept(newValue));
// this one is only slightly different
//ListView<String> options = new ListView<>();
//options.getSelectionModel().selectedItemProperty().addListener((o, oldValue, newValue) -> response.accept(newValue));
options.setItems(choices);
return setID(id, options);
}
// make group of radio choices
private Node makeRadioButtons (String idPrefix, ObservableList<String> choices, Consumer<String> response) {
//VBox result = new VBox();
HBox panel = new HBox();
panel.getStyleClass().add("choice-box");
ToggleGroup group = new ToggleGroup();
choices.forEach(c -> {
// these two components seems to work the same way
//ToggleButton btn = new ToggleButton(c);
RadioButton btn = new RadioButton(c);
btn.setToggleGroup(group);
panel.getChildren().add(setID(idPrefix + "-" + c, btn));
});
group.selectedToggleProperty().addListener((o, oldValue, newValue) -> response.accept(((Labeled)newValue.getToggleGroup().getSelectedToggle()).getText()));
return panel;
}
// make group of multiple choices
private Node makeCheckBoxes (String idPrefix, ObservableList<String> choices, Consumer<String> response) {
VBox panel = new VBox();
panel.getStyleClass().add("choice-box");
choices.forEach(c -> {
CheckBox cb = new CheckBox(c);
cb.selectedProperty().addListener((o, oldValue, newValue) -> response.accept(String.format("%s : %b", cb.getText(), newValue)));
panel.getChildren().add(setID(idPrefix + "-" + c, cb));
});
return panel;
}
private Node makeCanvas (String id, int width, int height, int shapeSize, Consumer<String> response) {
Pane canvas = new Pane();
canvas.setPrefSize(width,height);
canvas.setOnMouseClicked(e -> {
int x = (int)e.getX();
int y = (int)e.getY();
Circle c = new Circle(x, y, shapeSize / 2);
c.getStyleClass().add("ball");
canvas.getChildren().add(c);
response.accept(String.format("(%d, %d)", x, y));
// prevent any other nodes from dealing with this event
e.consume();
});
canvas.getStyleClass().add("canvas");
return setID(id, canvas);
}
// extract text from input field
private static void getText(TextField inputField, Consumer<String> response) {
response.accept(inputField.getText());
}
// extract text from dialog prompt
private void getInput (String prompt, Consumer<String> response) {
TextInputDialog dialog = new TextInputDialog(myResources.getString("InputPlaceholder"));
dialog.setTitle(myResources.getString("InputTitle"));
dialog.setContentText(myResources.getString(prompt));
Optional<String> result = dialog.showAndWait();
result.ifPresent(input -> response.accept(input));
}
// extract text from date file
private void getData (String dataFile, Consumer<String> response) {
List<String> lines = loadDataFile(dataFile);
IntStream.range(0, lines.size()).forEach(index -> myChoices.set(index, lines.get(index).strip()));
response.accept(dataFile);
}
// package friendly to allow for testing separately for errors
List<String> loadDataFile (String dataFile) {
try {
return Files.readAllLines(Paths.get(View.class.getClassLoader().getResource(dataFile).toURI()));
} catch (IOException | NullPointerException | URISyntaxException e) {
// should not happen if project is correctly configured
showError(String.format(myResources.getString("ResourceErrorMessage"), dataFile));
return Collections.emptyList();
}
}
// toggle CSS class used to display given node
private void toggleHighlight (Node n) {
final String CSS_CLASS = "highlight";
ObservableList<String> styles = n.getStyleClass();
if (styles.contains(CSS_CLASS)) {
styles.remove(CSS_CLASS);
}
else {
styles.add(CSS_CLASS);
}
}
// factor out this one line of code from being duplicated --- too much?
private Node setID (String id, Node node) {
node.setId(id);
return node;
}
// simple method to help debug, so all actions do the same thing
private void updateLabel (Label lbl, String txt) {
lbl.setText(txt);
if (DEBUG) {
System.out.println(lbl.getText());
}
}
}